// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io' show File;
import 'dart:typed_data' show Uint8List;

import 'package:_fe_analyzer_shared/src/parser/experimental_features.dart';
import 'package:_fe_analyzer_shared/src/parser/parser.dart';
import 'package:_fe_analyzer_shared/src/scanner/token.dart';
import 'package:_fe_analyzer_shared/src/scanner/utf8_bytes_scanner.dart';
import 'package:dart_style/dart_style.dart' show DartFormatter;

import '../test/utils/io_utils.dart'
    show computeRepoDirUri, getPackageVersionFor;

void main(List<String> args) {
  final Uri repoDir = computeRepoDirUri();
  String generated = generateAstHelper(repoDir);
  new File.fromUri(
    computeAstHelperUri(repoDir),
  ).writeAsStringSync(generated, flush: true);
}

Uri computeAstHelperUri(Uri repoDir) {
  return repoDir.resolve("pkg/front_end/lib/src/util/parser_ast_helper.dart");
}

String generateAstHelper(Uri repoDir) {
  StringBuffer out = new StringBuffer();
  File f = new File.fromUri(
    repoDir.resolve("pkg/_fe_analyzer_shared/lib/src/parser/listener.dart"),
  );
  Uint8List rawBytes = f.readAsBytesSync();
  Utf8BytesScanner scanner = new Utf8BytesScanner(
    rawBytes,
    includeComments: true,
  );
  Token firstToken = scanner.tokenize();

  out.write(r"""
// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:_fe_analyzer_shared/src/experiments/flags.dart';
import 'package:_fe_analyzer_shared/src/parser/assert.dart';
import 'package:_fe_analyzer_shared/src/parser/block_kind.dart';
import 'package:_fe_analyzer_shared/src/parser/constructor_reference_context.dart';
import 'package:_fe_analyzer_shared/src/parser/declaration_kind.dart';
import 'package:_fe_analyzer_shared/src/parser/formal_parameter_kind.dart';
import 'package:_fe_analyzer_shared/src/parser/identifier_context.dart';
import 'package:_fe_analyzer_shared/src/parser/listener.dart';
import 'package:_fe_analyzer_shared/src/parser/member_kind.dart';
import 'package:_fe_analyzer_shared/src/scanner/error_token.dart';
import 'package:_fe_analyzer_shared/src/scanner/token.dart';

import '../base/messages.dart';

// ignore_for_file: lines_longer_than_80_chars

// THIS FILE IS AUTO GENERATED BY
// 'tool/parser_ast_helper_creator.dart'
// Run this command to update it:
// 'dart pkg/front_end/tool/parser_ast_helper_creator.dart'

abstract class ParserAstNode {
  final String what;
  final ParserAstType type;
  Map<String, Object?> get deprecatedArguments;
  List<ParserAstNode>? children;
  ParserAstNode? parent;

  ParserAstNode(this.what, this.type);

  R accept<R>(ParserAstVisitor<R> v);

  void visitChildren(ParserAstVisitor v) {
    List<ParserAstNode>? children = this.children;
    if (children == null) return;
    for (ParserAstNode child in children) {
      child.accept(v);
    }
  }



  void debugPrint() {
    StringBuffer sb = new StringBuffer();
    _debugPrintImpl(0, sb);
    print(sb.toString());
  }

  void _debugPrintImpl(int indentation, StringBuffer sb) {
    sb.write(" " * indentation);
    sb.write(what);
    sb.write(type.name);
    Token? tokenWithSmallestOffset;
    for (Object? value in deprecatedArguments.values) {
      if (value is Token) {
        if (tokenWithSmallestOffset == null ||
            value.charOffset < tokenWithSmallestOffset.charOffset) {
          tokenWithSmallestOffset = value;
        }
      }
    }
    if (tokenWithSmallestOffset != null) {
      sb.write(
          " (${tokenWithSmallestOffset.lexeme} @ "
          "${tokenWithSmallestOffset.charOffset})");
    }
    sb.writeln();
    List<ParserAstNode>? children = this.children;
    if (children == null) return;
    for (ParserAstNode child in children) {
      child._debugPrintImpl(indentation + 2, sb);
    }
  }

  // TODO(jensj): Compare two ASTs.
}

abstract class BeginAndEndTokenParserAstNode implements ParserAstNode {
  Token get beginToken;
  Token get endToken;
}

enum ParserAstType { BEGIN, END, HANDLE }

abstract class AbstractParserAstListener implements Listener {
  List<ParserAstNode> data = [];

  void seen(ParserAstNode entry);

""");

  ParserCreatorListener listener = new ParserCreatorListener(out);
  ClassMemberParser parser = new ClassMemberParser(
    listener,
    experimentalFeatures: const DefaultExperimentalFeatures(),
  );
  parser.parseUnit(firstToken);

  out.writeln("}");
  out.writeln("");
  out.write(listener.newClasses.toString());

  out.write(r"abstract class ParserAstVisitor<R> {");
  for (String name in listener.visitNames) {
    out.write("  R $name;\n");
  }
  out.write(r"}");

  out.write(
    r"class RecursiveParserAstVisitor "
    "implements ParserAstVisitor<void> {",
  );
  for (String name in listener.visitNames) {
    out.write("  @override\n");
    out.write("  void $name => node.visitChildren(this);\n\n");
  }
  out.write(r"}");

  out.write(
    "class RecursiveParserAstVisitorWithDefaultNodeAsync "
    "implements ParserAstVisitor<Future<void>> {",
  );
  out.write("Future<void> defaultNode(ParserAstNode node) async {");
  out.write("  List<ParserAstNode>? children = node.children;");
  out.write("  if (children == null) return;");
  out.write("  for (ParserAstNode child in children) {");
  out.write("    await child.accept(this);");
  out.write("  }");
  out.write("}");

  for (String name in listener.visitNames) {
    out.write("  @override\n");
    out.write("  Future<void> $name => defaultNode(node);\n\n");
  }
  out.write(r"}");

  return new DartFormatter(
    languageVersion: getPackageVersionFor("front_end"),
  ).format("$out");
}

class ParserCreatorListener extends Listener {
  final StringSink out;
  bool insideListenerClass = false;
  String? currentMethodName;
  String? latestSeenParameterTypeToken;
  String? latestSeenParameterTypeTokenQuestion;
  final List<Parameter> parameters = <Parameter>[];
  Token? formalParametersEnd;
  final StringBuffer newClasses = new StringBuffer();
  final List<String> visitNames = [];

  ParserCreatorListener(this.out);

  @override
  void beginClassDeclaration(
    Token begin,
    Token? abstractToken,
    Token? macroToken,
    Token? sealedToken,
    Token? baseToken,
    Token? interfaceToken,
    Token? finalToken,
    Token? augmentToken,
    Token? mixinToken,
    Token name,
  ) {
    if (name.lexeme == "Listener") insideListenerClass = true;
  }

  @override
  void endClassDeclaration(Token beginToken, Token endToken) {
    insideListenerClass = false;
  }

  @override
  void beginConstructor(
    DeclarationKind declarationKind,
    Token? augmentToken,
    Token? externalToken,
    Token? staticToken,
    Token? covariantToken,
    Token? varFinalOrConst,
    Token? getOrSet,
    Token? newToken,
    Token name,
    String? enclosingDeclarationName,
  ) {
    currentMethodName = name.lexeme;
  }

  @override
  void beginMethod(
    DeclarationKind declarationKind,
    Token? augmentToken,
    Token? externalToken,
    Token? staticToken,
    Token? covariantToken,
    Token? varFinalOrConst,
    Token? getOrSet,
    Token name,
    String? enclosingDeclarationName,
  ) {
    currentMethodName = name.lexeme;
  }

  @override
  void endFormalParameters(
    int count,
    Token beginToken,
    Token endToken,
    MemberKind kind,
  ) {
    formalParametersEnd = endToken;
  }

  @override
  void endMethod(
    DeclarationKind kind,
    Token? getOrSet,
    Token beginToken,
    Token beginParam,
    Token? beginInitializers,
    Token endToken,
  ) {
    void end() {
      parameters.clear();
      currentMethodName = null;
      formalParametersEnd = null;
    }

    if (insideListenerClass &&
        (currentMethodName!.startsWith("begin") ||
            currentMethodName!.startsWith("end") ||
            currentMethodName!.startsWith("handle"))) {
      StringBuffer sb = new StringBuffer();
      sb.writeln("  @override");
      sb.write("  ");
      Token token = beginToken;
      Token? latestToken;
      if (formalParametersEnd == null) {
        // getter, so just copy through the getter name.
        formalParametersEnd = getOrSet!.next;
      }
      while (true) {
        if (latestToken != null && latestToken.charEnd < token.charOffset) {
          sb.write(" ");
        }
        sb.write(token.lexeme);
        if (latestToken == formalParametersEnd) break;
        if (token == endToken) {
          throw token.runtimeType;
        }
        latestToken = token;
        token = token.next!;
      }

      if (token is SimpleToken && token.type == TokenType.FUNCTION) {
        return end();
      } else {
        sb.write("\n    ");
        String typeString;
        String typeStringCamel;
        String name;
        if (currentMethodName!.startsWith("begin")) {
          typeString = "BEGIN";
          typeStringCamel = "Begin";
          name = currentMethodName!.substring("begin".length);
        } else if (currentMethodName!.startsWith("end")) {
          typeString = "END";
          typeStringCamel = "End";
          name = currentMethodName!.substring("end".length);
        } else if (currentMethodName!.startsWith("handle")) {
          typeString = "HANDLE";
          typeStringCamel = "Handle";
          name = currentMethodName!.substring("handle".length);
        } else {
          throw "Unexpected.";
        }

        String className = "${name}${typeStringCamel}";
        sb.write("$className data = new $className(");
        sb.write("ParserAstType.");
        sb.write(typeString);

        Set<String> nonQuestionParametersSet = {};

        for (int i = 0; i < parameters.length; i++) {
          Parameter param = parameters[i];
          sb.write(', ');
          sb.write(param.name);
          sb.write(': ');
          sb.write(param.name);

          if (!param.hasQuestion) {
            nonQuestionParametersSet.add("${param.type} ${param.name}");
          }
        }

        sb.write(");");
        sb.write("\n    ");
        sb.write("seen(data);");
        sb.write("\n  ");

        bool markBeginAndEndTokens = false;
        if (nonQuestionParametersSet.contains("Token beginToken") &&
            nonQuestionParametersSet.contains("Token endToken")) {
          newClasses.write(
            "class ${name}${typeStringCamel} "
            "extends ParserAstNode "
            "implements BeginAndEndTokenParserAstNode {\n",
          );
          markBeginAndEndTokens = true;
        } else {
          newClasses.write(
            "class ${name}${typeStringCamel} "
            "extends ParserAstNode {\n",
          );
        }

        for (int i = 0; i < parameters.length; i++) {
          Parameter param = parameters[i];
          if (markBeginAndEndTokens &&
              !param.hasQuestion &&
              param.type == "Token" &&
              (param.name == "beginToken" || param.name == "endToken")) {
            newClasses.writeln("  @override");
          }
          newClasses.write("  final ");
          newClasses.write(param.type);
          newClasses.write(param.hasQuestion ? '?' : '');
          newClasses.write(' ');
          newClasses.write(param.name);
          newClasses.write(';\n');
        }
        newClasses.write('\n');
        newClasses.write(
          "  ${name}${typeStringCamel}"
          "(ParserAstType type",
        );
        String separator = ", {";
        for (int i = 0; i < parameters.length; i++) {
          Parameter param = parameters[i];
          newClasses.write(separator);
          if (!param.hasQuestion) {
            newClasses.write('required ');
          }
          newClasses.write('this.');
          newClasses.write(param.name);
          separator = ", ";
        }
        if (parameters.isNotEmpty) {
          newClasses.write('}');
        }
        newClasses.write(') : super("$name", type);\n\n');
        newClasses.writeln("@override");
        newClasses.write("Map<String, Object?> get deprecatedArguments => {");
        for (int i = 0; i < parameters.length; i++) {
          Parameter param = parameters[i];
          newClasses.write('"');
          newClasses.write(param.name);
          newClasses.write('": ');
          newClasses.write(param.name);
          newClasses.write(',');
        }
        newClasses.write("};\n\n");

        newClasses.write("@override\n");
        newClasses.write("R accept<R>(ParserAstVisitor<R> v)");
        newClasses.write(" => v.visit$className(this);\n");
        visitNames.add("visit$className($className node)");

        newClasses.write("}\n");
      }

      sb.write("}");
      sb.write("\n\n");

      out.write(sb.toString());
    }
    end();
  }

  @override
  void handleNoType(Token lastConsumed) {
    latestSeenParameterTypeToken = null;
    latestSeenParameterTypeTokenQuestion = null;
  }

  @override
  void handleType(Token beginToken, Token? questionMark) {
    latestSeenParameterTypeToken = beginToken.lexeme;
    latestSeenParameterTypeTokenQuestion = questionMark?.lexeme;
  }

  @override
  void endFormalParameter(
    Token? varOrFinal,
    Token? thisKeyword,
    Token? superKeyword,
    Token? periodAfterThisOrSuper,
    Token nameToken,
    Token? initializerStart,
    Token? initializerEnd,
    FormalParameterKind kind,
    MemberKind memberKind,
  ) {
    parameters.add(
      new Parameter(
        nameToken.lexeme,
        latestSeenParameterTypeToken ?? 'dynamic',
        latestSeenParameterTypeTokenQuestion == null ? false : true,
      ),
    );
  }
}

class Parameter {
  final String name;
  final String type;
  final bool hasQuestion;

  Parameter(this.name, this.type, this.hasQuestion);
}
